/*
visqrt
Copyright (C) 2010  Bhupendra Aole

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
*/
/*
 * File: Scene.java
 * Author: Bhupendra Aole
 * Date: Aug 27, 2010: 
 * 
 * Revisions:
 * ----------
 */

package aole.object;

import java.awt.Color;
import java.util.ArrayList;

import aole.VQMain;
import aole.math.Color3;
import aole.math.Ray3;
import aole.math.Vec3;
import aole.math.noise.Perlin;
import aole.plugin.VQCamera;
import aole.plugin.VQLight;
import aole.plugin.VQScene;
import aole.texture.Texture3;
import aole.ui.VQRenderScreen;

public class Scene implements VQScene {
	// supersampling level (x = x*x times)
	public static final int sslevel = 3;
	// max depth level for reflection
	public static final int maxDepthLevel = 3;
	// backgroud color
	public Color3 bgcol = new Color3(0, 0, 0);
	// ambient color
	public Color3 ambcol = new Color3();
	// collection of renderable objects
	public ArrayList<Renderable> renderables = new ArrayList<Renderable>();
	// collection of lights
	public ArrayList<VQLight> lights = new ArrayList<VQLight>();
	// camera object
	public VQCamera cam;
	// render screen
	private VQRenderScreen screen;

	// set viewing ray
	private Ray3 viewRay = new Ray3();
	// small number for numerical precision
	private static final float fsmall = 0.01f;

	public void preCompute () {
		if (cam == null)
			cam = new CameraPinhole();
		if (lights.size() == 0) {
			lights.add(new PointLight(cam.getOrigin()));
		}
		cam.preCompute();
	}

	/* (non-Javadoc)
	 * @see aole.plugin.VQScene#addRenderable(aole.object.Renderable)
	 */
	public void addRenderable (Renderable renderable) {
		renderables.add(renderable);
	}

	/* (non-Javadoc)
	 * @see aole.plugin.VQScene#addTexture(aole.texture.Texture3)
	 */
	public void addTexture (Texture3 texture) {
	}

	/* (non-Javadoc)
	 * @see aole.plugin.VQScene#setCamera(aole.plugin.VQCamera)
	 */
	public void setCamera (VQCamera camera) {
		cam = camera;
	}

	/* (non-Javadoc)
	 * @see aole.plugin.VQScene#addLight(aole.plugin.VQLight)
	 */
	public void addLight (VQLight l) {
		lights.add(l);
	}

	public void render () throws InterruptedException {

		preCompute();

		int width = cam.getWidth();
		int height = cam.getHeight();

		screen = VQMain.getApplication().getRenderScreen(width, height);
		screen.display();

		// start the clock
		long starttime = System.currentTimeMillis();

		// viewing plane {-1,1 to 1,-1}
		// calculate delta
		float dy = -2f / height;
		float dx = 2f / width;

		// pixel color
		Color3 pcol = new Color3();

		// current x, y (starts at the viewing window top)
		float cx, cy = 1f;
		// was previous pixel sampled?
		// boolean prevsamp;

		// loop thru all the horizontal lines
		// starting from the top
		for (int line = 0; line < height; line++) {
			// set current x to the left of viewing window
			// when each line starts
			cx = -1f;
			// true for first pixel
			// prevsamp = true;
			// loop thru each pixel in the line
			// starting from the left
			for (int pixel = 0; pixel < width; pixel++) {
				// pcol.setZero();
				// create view vector
				cam.getViewRay(cx, cy, viewRay);
				getSampledColor(pcol, cx, cy, dx, dy);
				// gamma correction and photo exposure
				float cred = srgbEncode(pcol.r);
				float cgreen = srgbEncode(pcol.g);
				float cblue = srgbEncode(pcol.b);
				// set the pixel color
				screen.setPixel(pixel, line, new Color(cred, cgreen, cblue)
						.getRGB());
				cx += dx;// increment current x
			}
			cy += dy;// increment current y
			screen.updateImage(line);
		}
		// log, time taken
		System.out.println("Time taken: "
				+ (System.currentTimeMillis() - starttime) + " ms");
		// log, count of rays
		// System.out.println("Ray count: " + raycount);
		// sphere intersections
		System.out.println("Sphere intersections: " + Sphere.count);
		// Plane intersections
		System.out.println("Plane intersections: " + Plane.count);
		screen.repaint();
		// rf.setIgnoreRepaint(false);
	}

	private void getSampledColor (Color3 col, float cx, float cy, float dx,
			float dy) {
		float rx, ry = cy, cdx, cdy;
		cdx = dx / Scene.sslevel;
		cdy = dy / Scene.sslevel;

		for (int y = 0; y < Scene.sslevel; y++) {
			rx = cx;
			for (int x = 0; x < Scene.sslevel; x++) {
				cam.getViewRay(rx, ry, viewRay);
				col.add(trace2(null, viewRay, 1f, 0, Scene.maxDepthLevel));
				rx += cdx;
			}
			ry += cdy;
		}
		col.scale(1f / (Scene.sslevel * Scene.sslevel));
	}

	private float srgbEncode (float c) {
		// gamma correction
		/*
		if (c <= 0.0031308f) {
			return 12.92f * c;
		} else {
			// Inverse gamma 2.4
			return (float) (1.055 * Math.pow(c, 0.4166667) - 0.055);
		}*/
		// photo exposure
		return (float) (1f - Math.exp(-c));
		// simple clamping of the value
		// return Math.min(Math.max(c, 0f), 1f);
	}

	private Color3 trace2 (Renderable from, Ray3 vr, float n1, int inside,
			int depth) {
		// raycount++;
		// initialize with ambient color
		// only added if there is intersection with any object
		Color3 color = new Color3(ambcol);
		// color of the intersected object
		Color3 selfcol = new Color3();
		// color of the reflected/refracted surface
		Color3 refcol = new Color3();
		// set the closest intersection depth
		float minT = 1e5f;
		// initialize the closest object intersected
		Renderable closest = null;
		// reflection coef
		float reflcoef;
		// refraction coef
		float refrcoef;

		// loop thru' all the scene objects
		for (int i = 0; i < renderables.size(); i++) {
			Renderable obj = renderables.get(i);
			if (obj == from) {
				// continue;
			}
			// test each object for intersection
			float t = obj.calcIntersection(vr.orig, vr.dir);
			// if intersection is between near and far distance
			if (t > fsmall & t < minT) {
				minT = t;
				closest = obj;
			}
		}
		// if no intersection is found, return background
		// color.
		if (closest == null)
			return new Color3(bgcol);

		// intersection point
		Vec3 ip = Vec3.add(vr.orig, vr.dir.getScaled(minT));
		// normal at the intersection point
		Vec3 normal = closest.getNormal(ip);
		if (inside == 1)
			normal.scale(-1f);

		// addjust normal using bump mapping
		// TODO: replace with better architecture
		if (closest.mapping == Renderable.MAPPING_BUMP) {
			float lvl = 144f;
			float noiseCoefx = (float) (Perlin.noise(lvl * (ip.x),
					lvl * (ip.y), lvl * (ip.z)));
			float noiseCoefy = (float) (Perlin.noise(lvl * (ip.y),
					lvl * (ip.z), lvl * (ip.x)));
			float noiseCoefz = (float) (Perlin.noise(lvl * (ip.z),
					lvl * (ip.x), lvl * (ip.y)));

			normal.x = (1.0f - closest.level_bump) * normal.x
					+ closest.level_bump * noiseCoefx;
			normal.y = (1.0f - closest.level_bump) * normal.y
					+ closest.level_bump * noiseCoefy;
			normal.z = (1.0f - closest.level_bump) * normal.z
					+ closest.level_bump * noiseCoefz;

			float temp = Vec3.dot(normal, normal);
			if (temp != 0.0f) {
				temp = (float) (1.0 / Math.sqrt(temp));
				normal.scale(temp);
			}
		}

		reflcoef = closest.reflCoef;
		refrcoef = closest.refrCoef;
		if (reflcoef < 1f & refrcoef < 1f & inside == 0) {
			// shift intersection point by a small number towards normal
			Vec3 ip2 = ip;// .getAdded(normal.getScaled(fsmall));
			// get surface color
			// for each light in the scene
			for (int lgt = 0; lgt < lights.size(); lgt++) {
				// get light direction
				Vec3 l = lights.get(lgt).getPosition().getSubtracted(ip2);
				float lgtdist = l.length();
				l.normalize();
				// shadow testing: start
				// shadow ray
				// raycount++;
				boolean inShadow = false;
				for (int i = 0; i < renderables.size(); i++) {
					Renderable obj = renderables.get(i);
					if (obj == closest) {
						continue;
					}
					float t = obj.calcIntersection(ip2, l);
					if (t > fsmall && t < lgtdist) {
						inShadow = true;
						break;
					}
				}
				// shadow testing: end
				if (!inShadow) {
					// TODO: light source attinuation
					// Lambert shading
					float factor = Vec3.dot(normal, l) * closest.lamCoef;
					if (factor > 0) {
						Color3 temp = closest.getColor(ip).getScaled(factor);
						selfcol.add(temp);
					}

					// phong shading
					if (closest.illumination == Sphere.ILLUM_PHONG) {
						float reflet = Vec3.dot(l, normal) * 2f;
						Vec3 phongDir = l.getSubtracted(normal
								.getScaled(reflet));
						float phongTerm = Math.max(Vec3.dot(phongDir, vr.dir),
								0f);
						phongTerm = (float) (closest.specvalue * Math.pow(
								phongTerm, closest.specpower));
						selfcol.add(phongTerm);
					}
					// blinn-phong shanding
					else if (closest.illumination == Sphere.ILLUM_BLINN) {
						Vec3 blinnDir = l.getSubtracted(vr.dir);
						float temp = blinnDir.length();
						if (temp != 0) {
							blinnDir.scale(1f / temp);
							float blinnTerm = Math.max(Vec3.dot(blinnDir,
									normal), 0f);
							blinnTerm = (float) (closest.specvalue * Math.pow(
									blinnTerm, closest.specpower));
							selfcol.add(blinnTerm);
						}
					}
				}// shadow
			}// lights
			selfcol.scale(1f - reflcoef);
		}// if reflcoef<1
		color.add(selfcol);

		float cosi = Vec3.dot(vr.dir, normal);
		float n2 = closest.ior;
		// now reflected ray origins from the intersection point
		Ray3 vr2 = new Ray3();
		// get reflected color
		if (reflcoef > 0 & depth > 0 & refrcoef < 1f & inside == 0) {
			vr2.orig = ip;// .getAdded(normal.getScaled(fsmall));
			// direction is the reflection from the surface
			vr2.dir = vr.dir.getSubtracted(normal.getScaled(cosi * 2f));
			vr2.dir.normalize();
			refcol = trace2(closest, vr2, n1, inside, --depth);
			refcol.scale(reflcoef);
			color.add(refcol);
		}
		if (refrcoef > 0 & depth > 0) {
			vr2.orig = ip.getAdded(normal.getScaled(-fsmall));
			float n = n1 / n2;
			float sinT2 = n * n * (1 - cosi * cosi);
			assert sinT2 < 0;
			if (sinT2 <= 1f) {
				vr2.dir = vr.dir.getScaled(n);
				normal.scale((float) (n + Math.sqrt(1 - sinT2)));
				vr2.dir.subtract(normal);
				vr2.dir.normalize();
				refcol = trace2(closest, vr2, n2, inside == 0 ? 1 : 0, --depth);
				color.scale(1f - refrcoef);
				// TODO: combine diff. relection and refraction color correctly
				refcol.scale(refrcoef);
				color.add(refcol);
			}
		}
		return color;
	}

	/**
	 * 
	 */
	public void init () {
		renderables.clear();
		lights.clear();
		cam = null;
	}
}
